-
Notifications
You must be signed in to change notification settings - Fork 13.2k
feat: comprehensive Type Hierarchy LSP support #63052
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Implements the LSP Type Hierarchy protocol with support for: Core Features: - prepareTypeHierarchy: resolves declarations at cursor position - supertypes: returns base classes, interfaces, and type parameter constraints - subtypes: returns derived classes, implementing classes, and intersection types - On-demand loading: tree loads lazily one level at a time per LSP protocol Supported Declaration Types: - Classes (named and anonymous class expressions) - Interfaces (including declaration merging) - Type aliases (with semantic relationship tracking) - Mixin variables (const Mixed = Mixin(Base) patterns) - Type parameters (with constraint tracking) Kind Modifiers: - mixin, alias, conditional,extends, conditional,infer - intersection, union, mapped, tuple, template - indexed, keyof, readonly Performance: - FindAllReferences for efficient subtype lookup - Results limit (1000) to prevent issues in large codebases - Cancellation token support Tests: 27 fourslash tests covering all patterns All 99,267 tests passing AI Disclosure: Developed with GitHub Copilot (Claude Opus 4.5) assistance
|
Thanks for the PR! It looks like you've changed the TSServer protocol in some way. Please ensure that any changes here don't break consumers of the current TSServer API. For some extra review, we'll ping @sheetalkamat, @mjbvz, and @joj for you. Feel free to loop in other consumers/maintainers if necessary. |
|
Looks like you're introducing a change to the public API surface area. If this includes breaking changes, please document them on our wiki's API Breaking Changes page. Also, please make sure @DanielRosenwasser and @RyanCavanaugh are aware of the changes, just as a heads up. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR implements a comprehensive Type Hierarchy feature for the TypeScript language service, enabling IDE support for navigating class, interface, and type alias hierarchies through LSP Type Hierarchy protocol support.
Changes:
- Added core type hierarchy functionality supporting classes, interfaces, type aliases, mixins, and type parameters
- Implemented on-demand lazy loading following LSP protocol with supertype/subtype navigation
- Created comprehensive test suite with 27 fourslash tests covering various TypeScript patterns
Reviewed changes
Copilot reviewed 42 out of 66 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/services/typeHierarchy.ts | New core implementation file (~1000 lines) with hierarchy resolution, supertype/subtype collection, and mixin pattern support |
| src/services/types.ts | Added TypeHierarchyItem interface and three new LanguageService methods for type hierarchy protocol |
| src/services/services.ts | Integrated type hierarchy methods into language service with proper synchronization |
| src/services/_namespaces/* | Added TypeHierarchy namespace exports |
| src/server/session.ts | Added server-side protocol handlers for type hierarchy requests |
| src/server/protocol.ts | Added protocol definitions for type hierarchy commands and responses |
| src/harness/fourslashImpl.ts | Added test harness support with baseline formatting for type hierarchy |
| src/harness/fourslashInterfaceImpl.ts | Added verify API method for baseline testing |
| src/harness/client.ts | Added client-side type hierarchy methods with protocol conversion |
| tests/cases/fourslash/* | 27 new comprehensive test files covering all supported patterns |
| tests/baselines/reference/* | Baseline files for type hierarchy test outputs |
src/harness/fourslashImpl.ts
Outdated
| private formatTypeHierarchyItemSpan(file: FourSlashFile | undefined, item: ts.TypeHierarchyItem, span: ts.TextSpan, prefix: string, trailingPrefix = prefix) { | ||
| // For lib files, we don't have the source available in the test | ||
| if (!file) { | ||
| let text = ""; | ||
| text += `${prefix}╭ ${item.file} (lib file)\n`; | ||
| text += `${prefix}│ <source not available>\n`; | ||
| text += `${trailingPrefix}╰\n`; | ||
| return text; |
Copilot
AI
Jan 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The method name formatTypeHierarchyItemSpan is inconsistent with its implementation. It handles type hierarchy items but delegates to formatCallHierarchyItemSpan for non-lib files. Consider renaming to better reflect that it's a wrapper/adapter, or extract the lib file handling into a separate helper method.
| private formatTypeHierarchyItemSpan(file: FourSlashFile | undefined, item: ts.TypeHierarchyItem, span: ts.TextSpan, prefix: string, trailingPrefix = prefix) { | |
| // For lib files, we don't have the source available in the test | |
| if (!file) { | |
| let text = ""; | |
| text += `${prefix}╭ ${item.file} (lib file)\n`; | |
| text += `${prefix}│ <source not available>\n`; | |
| text += `${trailingPrefix}╰\n`; | |
| return text; | |
| private formatLibFileHierarchyItemSpan(item: ts.TypeHierarchyItem, prefix: string, trailingPrefix: string): string { | |
| // For lib files, we don't have the source available in the test | |
| let text = ""; | |
| text += `${prefix}╭ ${item.file} (lib file)\n`; | |
| text += `${prefix}│ <source not available>\n`; | |
| text += `${trailingPrefix}╰\n`; | |
| return text; | |
| } | |
| private formatTypeHierarchyItemSpan(file: FourSlashFile | undefined, item: ts.TypeHierarchyItem, span: ts.TextSpan, prefix: string, trailingPrefix = prefix) { | |
| if (!file) { | |
| return this.formatLibFileHierarchyItemSpan(item, prefix, trailingPrefix); |
src/harness/fourslashImpl.ts
Outdated
|
|
||
| public baselineTypeHierarchy(): void { | ||
| const item = this.languageService.prepareTypeHierarchy(this.activeFile.fileName, this.currentCaretPosition); | ||
| const text = item ? ts.mapOneOrMany(item, i => this.formatTypeHierarchy(i), result => result.join("")) : "none"; |
Copilot
AI
Jan 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The variable name i is too ambiguous for a callback parameter that represents a TypeHierarchyItem. Consider renaming to hierarchyItem or item for better clarity.
| const text = item ? ts.mapOneOrMany(item, i => this.formatTypeHierarchy(i), result => result.join("")) : "none"; | |
| const text = item ? ts.mapOneOrMany(item, hierarchyItem => this.formatTypeHierarchy(hierarchyItem), result => result.join("")) : "none"; |
src/services/services.ts
Outdated
| function provideTypeHierarchySupertypes(fileName: string, position: number): TypeHierarchyItem[] { | ||
| synchronizeHostData(); | ||
| const sourceFile = getValidSourceFile(fileName); | ||
| const declaration = TypeHierarchy.resolveTypeHierarchyDeclaration(program, position === 0 ? sourceFile : getTouchingPropertyName(sourceFile, position)); |
Copilot
AI
Jan 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The expression position === 0 ? sourceFile : getTouchingPropertyName(sourceFile, position) is duplicated in both provideTypeHierarchySupertypes and provideTypeHierarchySubtypes. Consider extracting this into a helper function to reduce code duplication.
src/services/services.ts
Outdated
| function provideTypeHierarchySupertypes(fileName: string, position: number): TypeHierarchyItem[] { | ||
| synchronizeHostData(); | ||
| const sourceFile = getValidSourceFile(fileName); | ||
| const declaration = TypeHierarchy.resolveTypeHierarchyDeclaration(program, position === 0 ? sourceFile : getTouchingPropertyName(sourceFile, position)); | ||
| return declaration ? TypeHierarchy.getSupertypes(program, declaration, cancellationToken) : []; | ||
| } | ||
|
|
||
| function provideTypeHierarchySubtypes(fileName: string, position: number): TypeHierarchyItem[] { | ||
| synchronizeHostData(); | ||
| const sourceFile = getValidSourceFile(fileName); | ||
| const declaration = TypeHierarchy.resolveTypeHierarchyDeclaration(program, position === 0 ? sourceFile : getTouchingPropertyName(sourceFile, position)); |
Copilot
AI
Jan 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The expression position === 0 ? sourceFile : getTouchingPropertyName(sourceFile, position) is duplicated in both provideTypeHierarchySupertypes and provideTypeHierarchySubtypes. Consider extracting this into a helper function to reduce code duplication.
| function provideTypeHierarchySupertypes(fileName: string, position: number): TypeHierarchyItem[] { | |
| synchronizeHostData(); | |
| const sourceFile = getValidSourceFile(fileName); | |
| const declaration = TypeHierarchy.resolveTypeHierarchyDeclaration(program, position === 0 ? sourceFile : getTouchingPropertyName(sourceFile, position)); | |
| return declaration ? TypeHierarchy.getSupertypes(program, declaration, cancellationToken) : []; | |
| } | |
| function provideTypeHierarchySubtypes(fileName: string, position: number): TypeHierarchyItem[] { | |
| synchronizeHostData(); | |
| const sourceFile = getValidSourceFile(fileName); | |
| const declaration = TypeHierarchy.resolveTypeHierarchyDeclaration(program, position === 0 ? sourceFile : getTouchingPropertyName(sourceFile, position)); | |
| function resolveTypeHierarchyDeclarationAtPosition(program: Program, sourceFile: SourceFile, position: number) { | |
| const node = position === 0 ? sourceFile : getTouchingPropertyName(sourceFile, position); | |
| return TypeHierarchy.resolveTypeHierarchyDeclaration(program, node); | |
| } | |
| function provideTypeHierarchySupertypes(fileName: string, position: number): TypeHierarchyItem[] { | |
| synchronizeHostData(); | |
| const sourceFile = getValidSourceFile(fileName); | |
| const declaration = resolveTypeHierarchyDeclarationAtPosition(program, sourceFile, position); | |
| return declaration ? TypeHierarchy.getSupertypes(program, declaration, cancellationToken) : []; | |
| } | |
| function provideTypeHierarchySubtypes(fileName: string, position: number): TypeHierarchyItem[] { | |
| synchronizeHostData(); | |
| const sourceFile = getValidSourceFile(fileName); | |
| const declaration = resolveTypeHierarchyDeclarationAtPosition(program, sourceFile, position); |
- Extract formatLibFileHierarchyItemSpan helper for lib file handling - Rename 'i' to 'hierarchyItem' for better clarity - Extract getTypeHierarchyNodeAtPosition to reduce duplication
|
@microsoft-github-policy-service agree |
- Add typeHierarchyMaxResults to UserPreferences (default: 1000) - Update type hierarchy API to accept preferences parameter - Pass preferences through services.ts and session.ts - Use configurable limit in getSubtypes for performance control - Document in PR_DESCRIPTION.md
This file is for PR documentation only, not part of the codebase
Document why mapped types like Required<T>, Pick<T, K> are not shown as subtypes: - Structural subtype checks are expensive and can produce noisy results - Mapped type semantics vary - some create subtypes, some create supertypes
|
Just so you don't spend more effort on this, we wouldn't accept this PR: #62827 You could theoretically send this to https://github.com/microsoft/typescript-go which actually has the LSP and everything in place to do something like this, or wait until this repo becomes the Go code. |
yes, I can do that, thx for the info! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 44 out of 67 changed files in this pull request and generated 3 comments.
| } | ||
|
|
||
| private formatTypeHierarchyItem(file: FourSlashFile | undefined, item: ts.TypeHierarchyItem, direction: TypeHierarchyItemDirection, seen: Map<string, boolean>, prefix: string, trailingPrefix: string = prefix): string { | ||
| const key = `${item.file}|${JSON.stringify(item.span)}|${direction}`; |
Copilot
AI
Jan 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using JSON.stringify for creating cache keys can be expensive when called frequently. Consider using a simpler concatenation like ${item.file}|${item.span.start}|${item.span.length}|${direction} for better performance.
| const key = `${item.file}|${JSON.stringify(item.span)}|${direction}`; | |
| const key = `${item.file}|${item.span.start}|${item.span.length}|${direction}`; |
| function getTypeHierarchyNodeAtPosition(sourceFile: SourceFile, position: number): Node { | ||
| return position === 0 ? sourceFile : getTouchingPropertyName(sourceFile, position); | ||
| } |
Copilot
AI
Jan 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This helper function duplicates logic from the call hierarchy implementation. Consider extracting a shared utility function to reduce code duplication.
|
|
||
| public baselineTypeHierarchy(): void { | ||
| const item = this.languageService.prepareTypeHierarchy(this.activeFile.fileName, this.currentCaretPosition); | ||
| const text = item ? ts.mapOneOrMany(item, hierarchyItem => this.formatTypeHierarchy(hierarchyItem), result => result.join("")) : "none"; |
Copilot
AI
Jan 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The inline arrow function and join callback make this line difficult to read. Consider breaking this into multiple lines or extracting the mapping logic for better clarity.
| const text = item ? ts.mapOneOrMany(item, hierarchyItem => this.formatTypeHierarchy(hierarchyItem), result => result.join("")) : "none"; | |
| let text: string; | |
| if (item) { | |
| const formatHierarchyItem = (hierarchyItem: ts.TypeHierarchyItem) => this.formatTypeHierarchy(hierarchyItem); | |
| const concatenateResults = (results: string[]) => results.join(""); | |
| text = ts.mapOneOrMany(item, formatHierarchyItem, concatenateResults); | |
| } | |
| else { | |
| text = "none"; | |
| } |
Type Hierarchy Feature: Comprehensive Enhancement
Summary
This PR implements a comprehensive Type Hierarchy feature for the TypeScript language service, providing IDE support for navigating class, interface, and type alias hierarchies. The feature supports the LSP Type Hierarchy protocol and enables rich navigation across supertypes and subtypes.
Fixes #45877
Features
Core Functionality
Supported Declaration Types
const Mixed = Mixin(Base)patterns)Advanced Type Patterns
The implementation handles complex TypeScript patterns:
extendsclausesimplementsclausesA & B)A | B)Note on Mapped Types: Utility types like
Required<T>,Pick<T, K>,Partial<T>are type-level transformations, not inheritance relationships. They don't appear as subtypes because structural subtype checks are expensive and can produce noisy results. The semantics vary - some create subtypes (Required), some create supertypes (Partial).Kind Modifiers
Types are annotated with descriptive
kindModifiersto help distinguish different type relationships:mixinconst Mixed = Mixin(Base)aliastype Foo = Barconditional,extendsT extends U ? X : Yconditional,inferT extends (...) => infer R ? R : neverintersectionA & BunionA | Bmapped{ [K in keyof T]: ... }tuple[A, B]template`Hello ${string}`indexedT["key"]keyofkeyof Treadonlyreadonly T[]Configurable Result Limits
The maximum number of results per level is configurable via
UserPreferences:This allows clients to adjust the limit based on their needs and prevents performance issues in very large codebases.
Implementation Details
Files Changed
src/services/typeHierarchy.ts(new, ~1000 lines)resolveTypeHierarchyDeclaration()- Entry point for type hierarchy requestscreateTypeHierarchyItem()- Creates hierarchy items with proper metadatagetSupertypes()- Collects base types, implemented interfaces, and type parameter constraintsgetSubtypes()- Finds derived types using hybrid approach with configurable limitsgetTypeHierarchyKindModifiers()- Returns modifiers based on type patternfindMixinVariablesUsingSymbol()- Reverse mixin lookupcollectTypeParameterConstraints()- Collects type parameter constraints as supertypessrc/compiler/types.ts(modified)typeHierarchyMaxResultstoUserPreferencesfor configurable result limitssrc/services/types.ts(modified)LanguageServiceinterface with preferences parametersrc/services/services.ts(modified)src/server/session.ts(modified)src/harness/fourslashImpl.ts(modified)kindModifiersdisplay in type hierarchy baselinesTest Files (27 new fourslash tests)
Key Algorithms
Supertype Collection
getEffectiveBaseTypeNode()for class inheritanceextends/implementsSubtype Collection (Hybrid Approach)
{ implementations: true }for efficient heritage clause lookupSymbol Resolution
skipAlias()getMergedSymbol()Mixin Support
The implementation recognizes and supports TypeScript mixin patterns:
Type Parameter Constraints
Type hierarchy shows type parameter constraints as supertypes:
Testing
Test Coverage
Running Tests
Semantic Correctness
Union vs Intersection Types
The implementation correctly models type relationships:
A & B): A subtype of both A and B (has ALL properties)A | B): A supertype of A and B (not a subtype!)Conditional Types
Conditional types like
ExtractDog<T> = T extends Dog ? T : neverare shown as "possible subtypes" when the condition could be satisfied. Theconditional,extendsorconditional,infermodifiers help distinguish these from structural subtypes.Mapped Types
Mapped types like
Required<T>,Partial<T>,Pick<T, K>are NOT shown as subtypes because:Required<T>creates subtypes,Partial<T>creates supertypesPerformance Considerations
Known Limitations
satisfiesis an expression-level operator, not a type-level relationshipFuture Enhancements
AI Disclosure
This PR was developed with assistance from GitHub Copilot (Claude Opus 4.5). The AI helped with:
All code has been reviewed by the contributor @kbrilla and tested against the full TypeScript test suite (99,267 tests passing).